﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class ReactiveCacheEnumerator : GroupCacheEnumeratorBase
  {
    #region Constructor

    internal ReactiveCacheEnumerator(
      ReactiveDataPathEnumerator reactiveDataPathEnumerator,
      ReactiveDataHelper dataHelper,
      int cacheSize )
      : base( new SyncToAsyncEnumerator<DataPath>( reactiveDataPathEnumerator ), dataHelper, dataHelper, dataHelper, cacheSize )
    {
      if( dataHelper == null )
        throw new ArgumentNullException( "dataHelper" );

      m_dataHelper = dataHelper;

      reactiveDataPathEnumerator.ShouldPerformFetchDelegate = this.CachedItems.Contains;
      reactiveDataPathEnumerator.OperationCompleted += new EventHandler<OperationCompletedEventArgs<DataPath>>( this.OnOperationCompleted );
    }

    #endregion

    #region ReactiveEnumerator Property

    private ReactiveDataPathEnumerator ReactiveEnumerator
    {
      get
      {
        return ( ReactiveDataPathEnumerator )
            ( ( SyncToAsyncEnumerator<DataPath> )
               this.ChildEnumerator ).ChildEnumerator;
      }
    }

    #endregion ReactiveEnumerator Property

    #region OperationCompleted Event

    internal event EventHandler<OperationCompletedEventArgs<DataPath>> OperationCompleted;

    private void OnOperationCompleted( object sender, OperationCompletedEventArgs<DataPath> e )
    {
      if( e == null )
        return;

      lock( this.SyncRoot )
      {
        if( this.MatchCurrentContext( e.Context ) )
        {
          if( e.Action == OperationCompletedAction.Items )
          {
            this.OnItemsOperationCompleted( e );
          }
          else
          {
            this.OnStatResultOperationCompleted( e );
          }
        }
      }

      if( this.OperationCompleted != null )
      {
        this.OperationCompleted( this, e );
      }
    }

    private void OnStatResultOperationCompleted( OperationCompletedEventArgs<DataPath> e )
    {
      // Must Create a copy of the "keys" since the dictionary can be modified in the loop
      IEnumerable<StatParameters> keys = this.CachedStatResults.Keys.ToList();
      foreach( StatParameters currentKey in keys )
      {
        StatResults statResults = this.CachedStatResults[ currentKey ];

        if( object.ReferenceEquals( e.DummyStatResults, statResults ) )
        {
          Debug.Assert( object.Equals( e.StatParameters, currentKey ), "Expecting same parameters" );
          this.CachedStatResults[ currentKey ] = e.RealStatResults;
        }
      }
    }

    private void OnItemsOperationCompleted( OperationCompletedEventArgs<DataPath> e )
    {
      if( e.Replacements != null )
      {
        foreach( Tuple<DataPath, DataPath> replacement in e.Replacements )
        {
          int index = this.CachedItems.IndexOf( replacement.Item1 );
          if( index != -1 )
          {
            this.CachedItems.RemoveAt( index );
            this.CachedItems.Insert( index, replacement.Item2 );
          }
        }
      }

      if( e.Deletions != null && e.Deletions.Count > 0 )
      {
        int minIndex = this.CachedItems.Count;
        int maxIndex = -1;

        foreach( DataPath path in e.Deletions )
        {
          int pathIndex = this.CachedItems.IndexOf( path );
          if( pathIndex >= 0 )
          {
            minIndex = Math.Min( minIndex, pathIndex );
            maxIndex = Math.Max( maxIndex, pathIndex );
          }
        }

        if( e.Operation.EndPositionObject.IsBeforeFirst )
        {
          if( maxIndex >= 0 )
          {
            this.CachedItems.RemoveRange( 0, maxIndex + 1 );
            this.UpdateCountInformation( new RelativeOffset( 1, ReferencePoint.BeforeFirst ), e.Operation.TotalCount );
          }
        }
        else
        {
          Debug.Assert( e.Operation.EndPositionObject.IsAfterLast );

          if( minIndex < this.CachedItems.Count )
          {
            this.CachedItems.RemoveRange( minIndex, this.CachedItems.Count - minIndex );
            this.UpdateCountInformation( new RelativeOffset( minIndex, ReferencePoint.AfterLast ), e.Operation.TotalCount );
          }
        }
      }
      else
      {
        EstimateInt64 notNeeded;
        RelativeOffset firstItemOffset;

        this.ExtractIndexes( e.Context, e.Operation, out notNeeded, out firstItemOffset );
        this.UpdateCountInformation( firstItemOffset, e.Operation.TotalCount );
      }

      //UI Specific behavior, spread values into cache.
      if( e.Replacements != null )
      {
        foreach( Tuple<DataPath, DataPath> replacement in e.Replacements )
        {
          this.SpreadNewValuesIntoCache( replacement.Item1, replacement.Item2 );
        }
      }
    }

    private void SpreadNewValuesIntoCache( DataPath oldItem, DataPath newItem )
    {
      int cacheIndex = this.CachedItems.IndexOf( newItem );

      if( cacheIndex < 0 )
        return;

      int cacheItemsCount = this.CachedItems.Count;

      Debug.Assert( newItem.Depth <= oldItem.Depth );

      DataPath newDummyTemplate = this.ReactiveEnumerator.DataItemFactory.CreateInstance( newItem );
      for( int i = cacheIndex - 1; i >= 0; i-- )
      {
        DataPath target = this.CachedItems[ i ];

        if( !this.InitializeParentFrom( target, newDummyTemplate ) )
          break;
      }

      //When the new Item is a collapsed group, New dummy sub-groups instance need to be done for other
      //direction; therefore, the newDummyTemplate is recreated.
      newDummyTemplate = this.ReactiveEnumerator.DataItemFactory.CreateInstance( newItem );
      for( int i = cacheIndex + 1; i < cacheItemsCount; i++ )
      {
        DataPath target = this.CachedItems[ i ];

        if( !this.InitializeParentFrom( target, newDummyTemplate ) )
          break;
      }
    }

    private bool InitializeParentFrom( DataPath target, DataPath source )
    {
      if( this.IsCompletelyInitialized( target ) )
        return false;

      Debug.Assert( !target[ target.Depth - 1 ].IsLoaded );

      int initCount = Math.Min( target.Depth, source.Depth ) - 1;

      for( int i = 0; i < initCount; i++ )
      {
        Debug.Assert( source[ i ].GetType() == target[ i ].GetType() );

        if( source[ i ].IsLoaded || !target[ i ].IsLoaded )
        {
          target[ i ] = source[ i ];
        }
      }
      return true;
    }

    private bool IsCompletelyInitialized( DataPath item )
    {
      //Start at the end because it is supposed to be the last
      //item initialized.
      for( int i = item.Depth - 1; i > -1; i-- )
      {
        if( !item[ i ].IsLoaded )
        {
          Debug.Assert( i == item.Depth - 1 );
          return false;
        }
      }

      return true;
    }

    #endregion OperationCompleted Event

    public void SetNextItemTemplate( DataPath template )
    {
      //A "null" itemTemplate is a valid template. That
      //is why there is a bool flag
      //to hold the "validity" of the item template value.
      m_useCacheNextItemTemplate = true;
      m_cacheNextItemTemplate = template;
    }

    public void SetPreviousItemTemplate( DataPath template )
    {
      //A "null" itemTemplate is a valid template. That
      //is why there is a bool flag
      //to hold the "validity" of the item template value.
      m_useCachePreviousItemTemplate = true;
      m_cachePreviousItemTemplate = template;
    }

    protected override JumpMoveAndFetchAsyncResult BeginChildJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback userCallback,
      object asyncState )
    {
      //Check if the data will be appended to the cache.
      int queryDirection = ( parameters.ExpectedMoveCount > 0 )
                             ? DataUtils.GetUnit( parameters.ExpectedMoveCount )
                             : DataUtils.GetUnit( parameters.ExpectedFetchCount );
      bool fetchCacheNext = false;
      bool fetchCachePrevious = false;

      if( this.CachedItems.Count == 0 )
      {
        fetchCacheNext = ( queryDirection > 0 );
        fetchCachePrevious = ( queryDirection < 0 );
      }
      else
      {
        Nullable<int> sourcePositionIndex = this.FindIndex(
          this.GetContext( parameters ),
          parameters.SourcePosition,
          parameters.ExpectedMoveCount,
          parameters.ExpectedFetchCount );

        if( sourcePositionIndex.HasValue )
        {
          long fetchStartIndex = sourcePositionIndex.Value + parameters.ExpectedMoveCount;
          long fetchEndIndex = fetchStartIndex + ( parameters.ExpectedFetchCount - DataUtils.GetUnit( parameters.ExpectedFetchCount ) );

          if( fetchStartIndex >= 0 && fetchStartIndex < this.CachedItems.Count )
            throw new DataInternalException();//Not expecting a child fetch starting within the bound of the cache.

          fetchCacheNext = ( fetchStartIndex <= this.CachedItems.Count ) && ( fetchEndIndex >= this.CachedItems.Count )
                        || ( fetchStartIndex >= this.CachedItems.Count ) && ( fetchEndIndex <= this.CachedItems.Count );

          fetchCachePrevious = ( fetchStartIndex <= 0 ) && ( fetchEndIndex >= 0 )
                            || ( fetchStartIndex >= 0 ) && ( fetchEndIndex <= 0 );
        }
      }

      //In theses case we consider the m_cacheNextItemTemplate and m_cachePreviousItemTemplate values
      if( fetchCacheNext && m_useCacheNextItemTemplate )
      {
        Debug.Assert( !fetchCachePrevious );

        //Cache will be append this data to the end of cache.
        this.ReactiveEnumerator.SetItemTemplateOverride( m_cacheNextItemTemplate );
      }
      else if( fetchCachePrevious && m_useCachePreviousItemTemplate )
      {
        Debug.Assert( !fetchCacheNext );

        //Cache will be append this data to the begin of cache.
        this.ReactiveEnumerator.SetItemTemplateOverride( m_cachePreviousItemTemplate );
      }

      JumpMoveAndFetchAsyncResult asyncResult = base.BeginChildJumpMoveAndFetch(
                                                  parameters,
                                                  userCallback,
                                                  asyncState );

      //The ItemOverride can now be cleared
      m_useCacheNextItemTemplate = false;
      m_useCachePreviousItemTemplate = false;
      m_cacheNextItemTemplate = null;
      m_cachePreviousItemTemplate = null;

      this.ReactiveEnumerator.ClearItemTemplateOverride();

      return asyncResult;
    }

    protected override void PathExpanded( DataPath path )
    {
      //If the collapsed path is in cache, we will flush all starting at this
      //item (ie. The collapsed path) will be flush by the base class.
      //
      // Then, Items fetched starting there will need to have this path as dummy template.

      int expandedPathIndex = this.CachedItems.IndexOf( path );
      if( expandedPathIndex != -1 )
      {
        this.SetNextItemTemplate( path );
      }

      base.PathExpanded( path );
    }

    protected override Nullable<int> FindIndex(
      DataSourceContext context,
      SourcePosition sourcePosition,
      long moveCount,
      int fetchCount )
    {
      if( sourcePosition is DummyPosition<DataPath> )
      {
        DataPath item;
        long offset;
        SourcePosition position;
        bool approximateOffset;
        DummyPosition<DataPath> dummyPosition = ( DummyPosition<DataPath> )sourcePosition;
        Func<DataPath, SourcePosition> positionResolver;

        if( DataUtils.ResolvePosition( dummyPosition, false, out position, out item, out offset, out approximateOffset, out positionResolver ) )
        {
          int index = -1;

          lock( this.SyncRoot )
          {
            if( this.MatchCurrentContext( context ) )
            {
              index = this.CachedItems.IndexOf( item );
            }
          }

          if( index >= 0 )
            return Convert.ToInt32( index + offset );

          return null;
        }
        else
        {
          Nullable<int> index = base.FindIndex( context, position, moveCount, fetchCount );

          if( index.HasValue )
            return Convert.ToInt32( index.Value + offset );

          return null;
        }
      }
      else if( sourcePosition is DependentPosition<DataPath> )
      {
        Debug.Assert( ( ( DependentPosition<DataPath> )sourcePosition ).PositionResolver != null,
          "If this is raised, a optimisation could be done by seaching the cache for the item." );

        return null;
      }

      return base.FindIndex( context, sourcePosition, moveCount, fetchCount );
    }

    protected override Nullable<int> FindIndex(
      DataSourceContext context,
      DataPath targetItem,
      long moveCount,
      int fetchCount )
    {
      lock( this.SyncRoot )
      {
        List<DataPath> items = this.CachedItems;

        if( ( items.Count == 0 )
          || ( !this.MatchCurrentContext( context ) ) )
          return null;

        int index = items.IndexOf( targetItem );

        if( index >= 0 )
          return index;

        if( m_dataHelper.IsDummy( targetItem ) )
          return null;




        int lowerBound;
        int upperBound;

        this.FindComparableItemsBounds( items, out lowerBound, out upperBound );

        // We cannot find a position if the whole cache contains dummies.
        if( lowerBound >= upperBound )
          return null;

        index = items.BinarySearch(
          lowerBound,
          upperBound - lowerBound,
          targetItem,
          new ItemComparer( context, m_dataHelper ) );

        // The item was found.
        if( index >= 0 )
          return index;

        index = ~index;

        return this.AdjustFindIndex( index, moveCount, fetchCount, true );
      }
    }

    protected override Nullable<int> FindIndex( DataSourceContext context, SortPosition sortPosition, long moveCount, int fetchCount )
    {
      lock( this.SyncRoot )
      {
        List<DataPath> items = this.CachedItems;

        if( ( items.Count == 0 )
          || ( !this.MatchCurrentContext( context ) ) )
          return null;

        int lowerBound;
        int upperBound;

        this.FindComparableItemsBounds( items, out lowerBound, out upperBound );

        // We cannot find a position if the whole cache contains dummies.
        if( lowerBound >= upperBound )
          return null;

        SortPositionComparer comparer = new SortPositionComparer( context, m_dataHelper, sortPosition );
        int index = items.BinarySearch( lowerBound, upperBound - lowerBound, comparer.Target, comparer );
        bool isItemPrecise = ( context.GetCurrentCompleteSortOrder().Count() == sortPosition.SortValues.Count );

        // The item was not found.
        if( index < 0 )
          return this.AdjustFindIndex( ~index, moveCount, fetchCount, isItemPrecise );

        if( isItemPrecise )
          return index;

        if( sortPosition.StartPosition == StartPosition.Before )
        {
          if( moveCount < 0 )
            return index + 1;

          if( ( moveCount == 0 ) && ( fetchCount < 0 ) )
            return index - 1;
        }
        else
        {
          if( moveCount > 0 )
            return index - 1;
        }

        return index;
      }
    }

    protected sealed override DataSourceContext GetCurrentContext()
    {
      return m_dataHelper.GetContext();
    }

    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if( this.ReactiveEnumerator.ShouldPerformFetchDelegate == this.CachedItems.Contains )
        {
          this.ReactiveEnumerator.ShouldPerformFetchDelegate = null; 
        }
        this.ReactiveEnumerator.OperationCompleted -= new EventHandler<OperationCompletedEventArgs<DataPath>>( OnOperationCompleted );
      }

      base.Dispose( disposing );
    }

    private void FindComparableItemsBounds( List<DataPath> items, out int lowerBound, out int upperBound )
    {
      if( items == null )
        throw new ArgumentNullException( "items" );

      lowerBound = int.MaxValue;
      upperBound = int.MinValue;

      if( items.Count == 0 )
        return;

      bool isFirstItemADummy = m_dataHelper.IsDummy( items.First() );
      bool isLastItemADummy = m_dataHelper.IsDummy( items.Last() );

      // We cannot find a position if the whole cache contains dummies.
      if( isFirstItemADummy && isLastItemADummy )
        return;

      lowerBound = 0;
      upperBound = items.Count;

      if( isFirstItemADummy || isLastItemADummy )
      {
        DummyDelimiter delimiter = new DummyDelimiter( m_dataHelper, isFirstItemADummy );
        int bound = items.BinarySearch( delimiter.Target, delimiter );

        // The fake target shouldn't be in the cache.
        Debug.Assert( bound < 0 );

        bound = ~bound;

        if( isFirstItemADummy )
        {
          lowerBound = bound;
        }
        else
        {
          upperBound = bound;
        }
      }
    }

    #region Private Fields

    private readonly ReactiveDataHelper m_dataHelper;
    private DataPath m_cacheNextItemTemplate;
    private DataPath m_cachePreviousItemTemplate;
    private bool m_useCacheNextItemTemplate;
    private bool m_useCachePreviousItemTemplate;

    #endregion

    #region ItemComparer Nested Type

    private class ItemComparer : IComparer<DataPath>
    {
      internal ItemComparer( DataSourceContext context, ReactiveDataHelper comparer )
      {
        if( context == null )
          throw new ArgumentNullException( "context" );

        if( comparer == null )
          throw new ArgumentNullException( "comparer" );

        m_context = context;
        m_comparer = comparer;
      }

      public int Compare( DataPath x, DataPath y )
      {
        if( m_comparer.IsDummy( x ) || m_comparer.IsDummy( y ) )
          throw new NotSupportedException();

        return m_comparer.Compare( m_context, x, y );
      }

      private readonly DataSourceContext m_context;
      private readonly ReactiveDataHelper m_comparer;
    }

    #endregion

    #region FakeTargetComparer Nested Type

    private abstract class FakeTargetComparer : IComparer<DataPath>
    {
      private static readonly DataPath FakeTarget = new DataPath( new DataItem[ 0 ] );

      protected FakeTargetComparer()
      {
      }

      public DataPath Target
      {
        get
        {
          return FakeTargetComparer.FakeTarget;
        }
      }

      public int Compare( DataPath x, DataPath y )
      {
        if( object.ReferenceEquals( x, this.Target ) )
          return this.CompareTarget( y );

        if( object.ReferenceEquals( y, this.Target ) )
          return -this.CompareTarget( x );

        return this.CompareItems( x, y );
      }

      protected abstract int CompareTarget( DataPath item );

      protected abstract int CompareItems( DataPath x, DataPath y );
    }

    #endregion

    #region SortPositionComparer Nested Type

    private class SortPositionComparer : FakeTargetComparer
    {
      internal SortPositionComparer( 
        DataSourceContext context, 
        ReactiveDataHelper comparer,
        SortPosition position )
      {
        if( context == null )
          throw new ArgumentNullException( "context" );

        if( comparer == null )
          throw new ArgumentNullException( "comparer" );

        if( position == null )
          throw new ArgumentNullException( "position" );

        m_context = context;
        m_comparer = comparer;
        m_position = position;
      }

      protected override int CompareTarget( DataPath item )
      {
        if( m_comparer.IsDummy( item ) )
          return -1;

        return m_comparer.Compare( m_context, m_position, item );
      }

      protected override int CompareItems( DataPath x, DataPath y )
      {
        if( m_comparer.IsDummy( x ) || m_comparer.IsDummy( y ) )
          return -1;

        return m_comparer.Compare( m_context, x, y );
      }

      private readonly DataSourceContext m_context;
      private readonly ReactiveDataHelper m_comparer;
      private readonly SortPosition m_position;
    }

    #endregion

    #region ItemComparer Nested Type

    private class DummyDelimiter : FakeTargetComparer
    {
      internal DummyDelimiter( ReactiveDataHelper comparer, bool isFirstItemADummy )
      {
        if( comparer == null )
          throw new ArgumentNullException( "comparer" );

        m_comparer = comparer;
        m_isFirstItemADummy = isFirstItemADummy;
      }

      protected override int CompareTarget( DataPath item )
      {
        return ( m_comparer.IsDummy( item ) == m_isFirstItemADummy ) ? 1 : -1;
      }

      protected override int CompareItems( DataPath x, DataPath y )
      {
        throw new InvalidOperationException();
      }

      private readonly ReactiveDataHelper m_comparer;
      private readonly bool m_isFirstItemADummy;
    }

    #endregion
  }
}
